﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;

namespace KvDbApi
{
    internal class KeyValueReaderStream : Stream
    {
        private readonly IDbConnection connection;

        private readonly ValueHandle handle;

        [ThreadStatic]
        private static byte[] singleByteBuffer;

        private SqlParameter versionParam;

        private SqlParameter expiryParam;

        private long totalBytes;

        private int fieldRemainder;

        private int fieldOffset;

        private IDataReader reader;

        private IDbCommand readerCommand;

        private bool hasValue;

        private ValueState valueState;

        public KeyValueReaderStream(IDbConnection connection, ValueHandle handle)
        {
            this.handle = handle;
            this.connection = connection;
        }

        public override bool CanRead
        {
            get
            {
                return true;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return false;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return false;
            }
        }

        public override long Length
        {
            get
            {
                throw new NotSupportedException("The length of the value is not known until the value is read.");
            }
        }

        public override long Position
        {
            get
            {
                return this.totalBytes;
            }

            set
            {
                throw Usage.SeekNotSupportedException();
            }
        }

        public override void Flush()
        {
        }

        public override void Close()
        {
            if (this.reader != null && !this.reader.IsClosed)
            {
                this.reader.Close();
                this.handle.SetValueMetadata(this.valueState, this.versionParam, this.expiryParam);
            }

            this.hasValue = false;
            if (this.readerCommand != null)
            {
                this.readerCommand.Dispose();
            }
        }

        public override int ReadByte()
        {
            // The underlying reader.GetBytes method still requires a buffer! 
            // Using a threadstatic buffer to address the allocation churn:
            if (singleByteBuffer == null)
            {
                singleByteBuffer = new byte[1];
            }

            return this.Read(singleByteBuffer, 0, 1) == 1 ? singleByteBuffer[0] : -1;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            buffer.ValidateForBufferOperation(offset, count);
            if (this.reader == null)
            {
                if (!this.TryKvGet())
                {
                    return 0;
                }

                this.hasValue = true;
            }

            if (!this.hasValue)
            {
                return 0;
            }

            var bytesRead = 0;
            while (count > 0 && this.hasValue)
            {
                if (this.fieldRemainder == 0)
                {
                    this.fieldRemainder = this.reader.GetInt32(0);
                    this.fieldOffset = 0;
                }

                var max = Math.Min(count, this.fieldRemainder);
                this.reader.GetBytes(1, this.fieldOffset, buffer, offset, max);

                this.fieldRemainder -= max;
                this.fieldOffset += max;
                count -= max;
                offset += max;
                bytesRead += max;

                // Reading has to lag as there is no other way to know if data exists in memory or on disk
                // when performing the initial KvGet()...the reader is expected to be positioned
                // on the first record when established. So reader.Read must lag in the loop, and only 
                // be done once the full buffer has been read:
                this.hasValue = this.fieldRemainder == 0 ? this.reader.Read() : true;
            }

            this.totalBytes += bytesRead;
            return bytesRead;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw Usage.SeekNotSupportedException();
        }

        public override void SetLength(long value)
        {
            throw Usage.SeekNotSupportedException();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw WriteNotSupportedException();
        }

        private static NotSupportedException WriteNotSupportedException()
        {
            return new NotSupportedException("Write is not supported when reading from the underlying key value store.");
        }

        private bool TryKvGet()
        {
            this.readerCommand = this.connection.CreateKvGet(this.handle, true, out this.versionParam, out this.expiryParam);
            this.reader = this.readerCommand.ExecuteReader();

            if (this.reader.Read())
            {
                this.valueState = ValueState.Memory;
                return true;
            }

            this.readerCommand.Cancel();
            this.reader.Close();
            this.readerCommand.Dispose();

            this.readerCommand = this.connection.CreateKvGet(this.handle, false, out this.versionParam, out this.expiryParam);
            this.reader = this.readerCommand.ExecuteReader();
            this.valueState = ValueState.Storage;
            if (!this.reader.Read())
            {
                this.Close();
                this.hasValue = false;
                this.handle.SetNotFound();
                return false;
            }

            return true;
        }
    }
}
